<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>AI 对话</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    /* 简洁滚动条 */
    ::-webkit-scrollbar { width: 8px; height: 8px; }
    ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
    ::-webkit-scrollbar-track { background: #f1f5f9; }
  </style>
</head>
<body class="bg-slate-50 text-slate-800">
  <!-- 整体布局 -->
  <div class="h-screen flex">
    <!-- 左侧聊天列表 -->
    <aside class="w-80 border-r border-slate-200 bg-white flex flex-col">
      <div class="p-4 border-b border-slate-200">
        <div class="flex items-center gap-2">
          <button id="btnNewChat" class="px-3 py-2 rounded bg-blue-600 text-white hover:bg-blue-700">新建聊天</button>
          <button id="btnToggleList" class="px-3 py-2 rounded bg-slate-100 hover:bg-slate-200">展开/收起</button>
        </div>
        <div class="mt-3">
          <label class="block text-sm mb-1" for="kbSelect">知识库</label>
          <select id="kbSelect" class="w-full px-3 py-2 border rounded focus:outline-none focus:ring focus:ring-blue-200">
            <option value="">不使用知识库</option>
          </select>
        </div>
      </div>
      <div id="chatList" class="flex-1 overflow-y-auto">
        <!-- 动态填充聊天项 -->
      </div>
    </aside>

    <!-- 右侧对话区 -->
    <main class="flex-1 flex flex-col">
      <!-- 当前聊天标题与操作 -->
      <div class="p-4 border-b border-slate-200 bg-white flex items-center justify-between">
        <div>
          <h1 id="currentChatTitle" class="text-lg font-semibold">未选择聊天</h1>
          <p class="text-xs text-slate-500">提示：Ctrl+Enter 发送</p>
        </div>
        <div class="flex items-center gap-2">
          <button id="btnRenameChat" class="px-3 py-2 rounded bg-slate-100 hover:bg-slate-200" disabled>重命名</button>
          <button id="btnDeleteChat" class="px-3 py-2 rounded bg-red-50 text-red-600 hover:bg-red-100" disabled>删除</button>
        </div>
      </div>

      <!-- 消息列表 -->
      <div id="messageList" class="flex-1 overflow-y-auto p-6 space-y-4">
        <!-- 动态填充消息 -->
      </div>

      <!-- 输入区 -->
      <div class="p-4 border-t border-slate-200 bg-white">
        <div class="flex gap-3 items-end">
          <div class="flex-1">
            <label class="block text-sm mb-1" for="modelInput">模型</label>
            <input id="modelInput" type="text" value="deepseek-r1:1.5b" class="w-full px-3 py-2 border rounded focus:outline-none focus:ring focus:ring-blue-200" />
          </div>
          <div class="flex-1">
            <label class="block text-sm mb-1" for="messageInput">内容</label>
            <textarea id="messageInput" rows="3" class="w-full px-3 py-2 border rounded focus:outline-none focus:ring focus:ring-blue-200" placeholder="输入对话内容，Ctrl+Enter 发送"></textarea>
          </div>
          <div class="flex-none">
            <button id="btnSend" class="px-4 py-2 rounded bg-blue-600 text-white hover:bg-blue-700">发送</button>
          </div>
        </div>
      </div>
    </main>
  </div>

  <script>
    // 后端服务基础地址
    const BASE_URL = 'http://localhost:8090';
    const API_RAG_TAGS = BASE_URL + '/api/v1/rag/query_rag_tag_list';
    const API_STREAM_GET = BASE_URL + '/api/v1/ollama/generate_stream'; // GET: model, message
    const API_STREAM_RAG_POST = BASE_URL + '/api/v1/ollama/generate_stream_rag'; // POST: {model, ragTag, message}

    // 状态管理（本地存储持久化）
    const state = {
      chats: [], // {id, title, messages: [{role:'user'|'assistant', content: string}]}
      currentId: null,
      expanded: true,
    };

    function loadState() {
      try {
        const raw = localStorage.getItem('ai_chats');
        if (raw) {
          const data = JSON.parse(raw);
          state.chats = Array.isArray(data.chats) ? data.chats : [];
          state.currentId = data.currentId || null;
        }
      } catch (e) { console.warn('加载状态失败', e); }
    }
    function saveState() {
      localStorage.setItem('ai_chats', JSON.stringify({ chats: state.chats, currentId: state.currentId }));
    }

    // 工具函数
    function uid() { return 'c_' + Math.random().toString(36).slice(2); }
    function nowTitle() { return '新的对话 ' + new Date().toLocaleTimeString(); }
    function currentChat() { return state.chats.find(c => c.id === state.currentId) || null; }

    // UI 渲染
    function renderChatList() {
      const list = document.getElementById('chatList');
      list.innerHTML = '';
      const container = document.createElement('div');
      container.className = state.expanded ? 'divide-y divide-slate-200' : 'hidden';

      state.chats.forEach(chat => {
        const item = document.createElement('div');
        const active = chat.id === state.currentId;
        item.className = 'p-3 cursor-pointer hover:bg-slate-50 ' + (active ? 'bg-slate-100' : '');
        item.innerHTML = `
          <div class="flex items-center justify-between">
            <div class="font-medium truncate">${chat.title}</div>
            <div class="text-xs text-slate-500">${chat.messages?.length || 0} 条</div>
          </div>
        `;
        item.addEventListener('click', () => {
          state.currentId = chat.id;
          saveState();
          renderAll();
        });
        container.appendChild(item);
      });

      list.appendChild(container);
    }

    function renderCurrentChatHeader() {
      const titleEl = document.getElementById('currentChatTitle');
      const btnRename = document.getElementById('btnRenameChat');
      const btnDelete = document.getElementById('btnDeleteChat');
      const chat = currentChat();
      if (!chat) {
        titleEl.textContent = '未选择聊天';
        btnRename.disabled = true;
        btnDelete.disabled = true;
        return;
      }
      titleEl.textContent = chat.title;
      btnRename.disabled = false;
      btnDelete.disabled = false;
    }

    function renderMessages() {
      const box = document.getElementById('messageList');
      box.innerHTML = '';
      const chat = currentChat();
      if (!chat) {
        const empty = document.createElement('div');
        empty.className = 'text-slate-500 text-sm';
        empty.textContent = '请选择左侧一个聊天或创建新聊天';
        box.appendChild(empty);
        return;
      }

      chat.messages.forEach(msg => {
        const bubble = document.createElement('div');
        const isUser = msg.role === 'user';
        bubble.className = 'max-w-3xl px-4 py-3 rounded shadow-sm ' + (isUser ? 'bg-blue-50 border border-blue-100' : 'bg-slate-50 border border-slate-200');
        bubble.innerHTML = `
          <div class="text-xs ${isUser ? 'text-blue-600' : 'text-slate-600'} mb-1">${isUser ? '我' : 'AI'}</div>
          <div class="whitespace-pre-wrap leading-relaxed">${escapeHtml(msg.content || '')}</div>
        `;
        box.appendChild(bubble);
      });

      box.scrollTop = box.scrollHeight;
    }

    function renderAll() {
      renderChatList();
      renderCurrentChatHeader();
      renderMessages();
    }

    function escapeHtml(str) {
      return (str || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    }

    // 事件与操作
    document.getElementById('btnNewChat').addEventListener('click', () => {
      const id = uid();
      const chat = { id, title: nowTitle(), messages: [] };
      state.chats.unshift(chat);
      state.currentId = id;
      saveState();
      renderAll();
    });

    document.getElementById('btnToggleList').addEventListener('click', () => {
      state.expanded = !state.expanded;
      renderChatList();
    });

    document.getElementById('btnRenameChat').addEventListener('click', () => {
      const chat = currentChat();
      if (!chat) return;
      const name = prompt('请输入新的聊天名称', chat.title);
      if (name && name.trim()) {
        chat.title = name.trim();
        saveState();
        renderAll();
      }
    });

    document.getElementById('btnDeleteChat').addEventListener('click', () => {
      const chat = currentChat();
      if (!chat) return;
      if (confirm('确认删除当前聊天？')) {
        state.chats = state.chats.filter(c => c.id !== chat.id);
        if (state.currentId === chat.id) state.currentId = null;
        saveState();
        renderAll();
      }
    });

    // 发送逻辑：EventSource（GET）与 fetch 流（POST RAG）兼容实现
    document.getElementById('btnSend').addEventListener('click', sendMessage);
    document.getElementById('messageInput').addEventListener('keydown', (e) => {
      if (e.key === 'Enter' && e.ctrlKey) {
        e.preventDefault();
        sendMessage();
      }
    });

    function ensureChat() {
      if (!currentChat()) {
        const id = uid();
        const chat = { id, title: nowTitle(), messages: [] };
        state.chats.unshift(chat);
        state.currentId = id;
      }
    }

    function sendMessage() {
      ensureChat();
      const chat = currentChat();
      const model = document.getElementById('modelInput').value.trim();
      const message = document.getElementById('messageInput').value.trim();
      const ragTag = document.getElementById('kbSelect').value.trim();
      if (!message) return;

      // 添加用户消息
      chat.messages.push({ role: 'user', content: message });
      // 预先插入一个空的 AI 消息，用来流式拼接
      const aiMsg = { role: 'assistant', content: '' };
      chat.messages.push(aiMsg);
      saveState();
      renderAll();

      // 根据是否选择知识库，选择调用方式
      if (ragTag) {
        // 优先使用 GET + SSE 的 RAG 接口
        streamWithEventSourceRag(model, ragTag, message, aiMsg);
      } else {
        streamWithEventSource(model, message, aiMsg);
      }
      document.getElementById('messageInput').value = '';
    }

    // SSE: EventSource（GET）: /generate_stream?model=&message=
    function streamWithEventSource(model, message, aiMsg) {
      const url = new URL(API_STREAM_GET);
      url.searchParams.set('model', model);
      url.searchParams.set('message', message);

      const eventSource = new EventSource(url.toString());
      let closed = false;

      eventSource.onmessage = (event) => {
        try {
          const data = JSON.parse(event.data);
          const content = extractContent(data);
          const finished = isFinished(data);
          if (content) {
            aiMsg.content += content;
            saveState();
            renderMessages();
          }
          if (finished) {
            eventSource.close();
            closed = true;
          }
        } catch (e) {
          console.warn('SSE 数据解析失败', e, event.data);
        }
      };

      eventSource.onerror = () => {
        if (!closed) {
          eventSource.close();
        }
      };
    }

    // POST 流式（RAG）：/generate_stream_rag，读取 Response.body 流
    async function streamWithPostRag(model, ragTag, message, aiMsg) {
      try {
        const resp = await fetch(API_STREAM_RAG_POST, {
          method: 'POST',
          // 明确跨域与流式偏好
          mode: 'cors',
          cache: 'no-store',
          headers: {
            'Content-Type': 'application/json',
            'Accept': 'text/event-stream, application/stream+json, application/json'
          },
          body: JSON.stringify({ model, ragTag, message })
        });

        if (!resp.ok) {
          const bodyText = await resp.text().catch(() => '');
          console.error('POST 流式请求非200', resp.status, resp.statusText, bodyText);
          throw new Error('请求失败或不支持流式');
        }
        if (!resp.body) {
          console.error('响应不支持ReadableStream，Content-Type:', resp.headers.get('Content-Type'));
          throw new Error('请求失败或不支持流式');
        }

        const reader = resp.body.getReader();
        const decoder = new TextDecoder('utf-8');
        let buffer = '';
        while (true) {
          const { value, done } = await reader.read();
          if (done) break;
          buffer += decoder.decode(value, { stream: true });
          // 兼容 SSE 的 data: 行，以及 NDJSON 按行切分
          const lines = buffer.split(/\r?\n/);
          buffer = lines.pop(); // 保留未完成行
          for (const line of lines) {
            const payload = line.startsWith('data:') ? line.slice(5).trim() : line.trim();
            if (!payload) continue;
            try {
              const data = JSON.parse(payload);
              const content = extractContent(data);
              const finished = isFinished(data);
              if (content) {
                aiMsg.content += content;
                saveState();
                renderMessages();
              }
              if (finished) {
                // 结束标识，直接完成
                buffer = '';
                return;
              }
            } catch (e) {
              // 非 JSON 片段忽略
            }
          }
        }
      } catch (e) {
        console.error('POST 流式请求失败', e);
        // 回退：提示并使用非RAG的GET流式，避免阻塞用户
        const notice = `RAG 流式失败：${e?.message || e}. 将使用普通流式回答。`;
        aiMsg.content += `\n\n${notice}\n`;
        saveState();
        renderMessages();
        streamWithEventSource(model, message, aiMsg);
      }
    }

    // RAG 的 GET + SSE：/generate_stream_rag?model=&ragTag=&message=
    function streamWithEventSourceRag(model, ragTag, message, aiMsg) {
      const url = new URL(API_STREAM_GET.replace('generate_stream', 'generate_stream_rag'));
      url.searchParams.set('model', model);
      url.searchParams.set('ragTag', ragTag);
      url.searchParams.set('message', message);

      const eventSource = new EventSource(url.toString());
      let closed = false;

      eventSource.onmessage = (event) => {
        try {
          const data = JSON.parse(event.data);
          const content = extractContent(data);
          const finished = isFinished(data);
          if (content) {
            aiMsg.content += content;
            saveState();
            renderMessages();
          }
          if (finished) {
            eventSource.close();
            closed = true;
          }
        } catch (e) {
          console.warn('RAG SSE 数据解析失败', e, event.data);
        }
      };

      eventSource.onerror = () => {
        if (!closed) {
          eventSource.close();
        }
        // 回退到 POST 流式
        streamWithPostRag(model, ragTag, message, aiMsg);
      };
    }

    // 从响应中提取文本，兼容多种结构
    function extractContent(data) {
      try {
        if (!data) return '';
        // 结构1：{ result: { output: { content } } }
        if (data.result && data.result.output) {
          const c = data.result.output.content;
          return c || '';
        }
        // 结构2：{ results: [ { output: { content } } ] }
        if (Array.isArray(data.results) && data.results.length > 0) {
          const c = data.results[0]?.output?.content;
          return c || '';
        }
        return '';
      } catch { return ''; }
    }

    function isFinished(data) {
      try {
        const finish = data?.result?.metadata?.finishReason || data?.results?.[0]?.metadata?.finishReason;
        return finish === 'STOP';
      } catch { return false; }
    }

    // 加载知识库选项
    async function loadKnowledgeTags() {
      try {
        const resp = await fetch(API_RAG_TAGS);
        const json = await resp.json();
        const data = Array.isArray(json?.data) ? json.data : [];
        const sel = document.getElementById('kbSelect');
        // 清空除第一项
        sel.querySelectorAll('option:not(:first-child)').forEach(o => o.remove());
        data.forEach(tag => {
          const opt = document.createElement('option');
          opt.value = tag;
          opt.textContent = tag;
          sel.appendChild(opt);
        });
      } catch (e) {
        console.warn('知识库列表加载失败', e);
      }
    }

    // 初始化
    loadState();
    renderAll();
    loadKnowledgeTags();
  </script>
</body>
</html>
